import { getLogger, numericCompare, tuple, sleep, truthy, Service } from '@gnlab/utils'
import { MemSubject } from '@gnlab/utils/rx'
import { OrderedMap } from 'immutable'
import { v4 as uuid } from 'uuid'
import { KVStorageManager } from '@/lib/storage'
import type { Services } from '@/services'
import { API } from '@/services/common/api'

const logger = getLogger('services/batch-login')

const CONCURRENCY = 3 // 一次性登录几个企业微信
const DELAY_SECONDS = 10 // 一个企业微信登录成功后，过多少秒登录下一个

// 设备状态
export enum Status {
    Idle = 1, // 空闲设备，未被选中
    Queueing = 2, // 处在登录队列中，但还没开始登录
    LoggingIn = 3, // 正在进行登录流程（请求容器、执行登录...）
    Failed = 4, // 登录失败
    LoggedIn = 5, // 已登录
}

interface BaseDevice {
    id: string
    name: string
}
export interface NotLoggedInDevice extends BaseDevice {
    status: Status.Idle | Status.Queueing | Status.LoggingIn
    hostname?: string // 记录里有，但当前找不到对应企业微信的 hostname
}
export interface LoggedInDevice extends BaseDevice {
    status: Status.LoggedIn
    hostname: string // 对应的容器 hostname
}
export interface FailedDevice extends BaseDevice {
    status: Status.Failed
    error: string
}
export type Device = NotLoggedInDevice | LoggedInDevice | FailedDevice
type Devices = OrderedMap<string, Device>

type TaskStatus = { started: true; taskId: string } | { started: false; error: string | null }

export class BatchLogin extends Service<Services> {
    private get container() {
        return this.services.container
    }
    private get qywxs() {
        return this.services.qywxs
    }

    registered() {
        this.initListenQywxStatus()
    }

    protected _status = new MemSubject<TaskStatus>({ started: false, error: null })
    status = this._status.asStream()

    protected _devices = new MemSubject<Devices>(OrderedMap()) // 为 null 代表初始加载中
    devices = this._devices.asStream()

    protected get started() {
        return this.status.value.started
    }
    protected get taskId() {
        return this.status.value.started ? this.status.value.taskId : null
    }

    // 持久化记录登录过的设备的信息
    // deviceId => hostname
    protected storage = new KVStorageManager<string>({
        topic: 'service-batch-login',
        defaults: {},
    })

    // 确认指定 hostname 是否还有有效的企业微信登录着
    protected confirmHostname(hostname: string) {
        for (const qywx of this.qywxs.qywxs.value.values()) {
            if (qywx.hostname === hostname) return true
        }
        return false
    }

    compareId(a: Device, b: Device) {
        return numericCompare(a.id, b.id)
    } // 根据 id 进行排序
    compareName(a: Device, b: Device) {
        return numericCompare(a.name, b.name)
    } // 根据 name 进行排序
    compareStatus(a: Device, b: Device) {
        return a.status - b.status
    } // 根据 status 进行排序

    /**
     * 加载待登录设备
     * 成功，返回 undefined；失败，返回错误消息
     */
    async loadDevices() {
        const res =
            await API<{ deviceId: string; deviceName: string | null }[]>('/Device/GetDeviceList')
        if (res.success) {
            // 生成新设备列表，保留原列表中设备的状态，抛弃新列表里没有再出现的设备
            const list = res.data.map(this.formatDevice).map(device => tuple(device.id, device))
            list.sort((a, b) => this.compareName(a[1], b[1]) || this.compareId(a[1], b[1])) // 列表初始即按先名称次 id 来排序
            this._devices.next(OrderedMap(list))
            if (!this.status.value.started && truthy(this.status.value.error))
                this._status.next({ started: false, error: null }) // 清除错误信息
        } else {
            logger.warn('获取设备列表失败：', res.error)
            // 如果当前已启动登录流程，列表加载失败会将流程停止
            this._status.next({ started: false, error: res.error || '未知错误' })
        }
    }

    protected formatDevice = (raw: { deviceId: string; deviceName: string | null }): Device => {
        const { deviceId: id, deviceName: rawName } = raw
        const name = (rawName ?? '') || id

        // 若设备之前已拉取到，在原状态还有效的情况下，保留原状态，不过名字始终用最新的
        const prev = this.devices.value.get(id) ?? null
        if (prev) {
            return prev.status === Status.LoggedIn && !this.confirmHostname(prev.hostname)
                ? { ...prev, name, status: Status.Idle }
                : { ...prev, name }
        }

        const hostname = this.storage.get(id)
        return truthy(hostname) && this.confirmHostname(hostname)
            ? { id, name, status: Status.LoggedIn, hostname }
            : { id, name, status: Status.Idle, hostname }
    }

    /**
     * 切换设备登录状态
     * 此操作对 LoggingIn、LoggedIn 的设备无效；Failed 的设备会变为 Queueing
     */
    choose(id: string) {
        const device = this.devices.value.get(id)
        if (!device) return void logger.info(`未找到设备 id=${id}`)
        if (device.status === Status.LoggingIn || device.status === Status.LoggedIn)
            return void logger.info('设备状态不符合要求，无法切换状态', id, device)

        const status =
            device.status === Status.Idle || device.status === Status.Failed
                ? Status.Queueing
                : Status.Idle
        this._devices.next(this.devices.value.set(id, { id: device.id, name: device.name, status }))
    }

    /**
     * 选中 / 取消选中所有可以进行选择的设备
     * Idle 和 Queueing 状态的设备会被切换，Failed 的设备视为 Idle 状态
     */
    chooseAll() {
        let devices = this.devices.value
        const targets = [...devices.values()].filter(device =>
            [Status.Idle, Status.Queueing, Status.Failed].includes(device.status),
        )
        const targetStatus = targets.find(device =>
            [Status.Idle, Status.Failed].includes(device.status),
        )
            ? Status.Queueing
            : Status.Idle

        targets.forEach(device => {
            devices = devices.set(device.id, { id: device.id, name: device.name, status: targetStatus })
        })

        this._devices.next(devices)
    }

    /**
     * 启动登录流程
     */
    start() {
        if (this.started) return

        const taskId = uuid()
        this._status.next({ started: true, taskId })

        // 允许并发登录几个企业微信，就执行几个 loop
        // 并行的 loop 之间利用 setTimeout() 保持一点时间差
        for (let i = 0; i < CONCURRENCY; i++) {
            setTimeout(async () => this.loop(taskId, i), i * 3000)
        }
    }

    /**
     * 停止登录流程（不会影响正在登录或已登录的设备）
     */
    stop() {
        this._status.next({ started: false, error: null })
    }

    /**
     * 循环执行登录
     *
     * loopIndex: 一个 task 会启动多个并发的 loop，此值代表了当前调用是第几个 loop
     */
    protected async loop(taskId: string, loopIndex: number) {
        /* eslint-disable no-await-in-loop */
        while (this.taskId === taskId) {
            // 只要 taskId 还没变，说明循环还有效
            // 取得最前面的待登录设备
            const device = this.devices.value.find(_device => _device.status === Status.Queueing)

            // 没有待登录设备，一秒钟后重试
            if (!device) {
                logger.debug(`task #${taskId} loop #${loopIndex}: no device to login`)
                await sleep(1000)
                continue
            }

            // 将设备标记为“登录中”
            logger.debug(`task #${taskId} loop #${loopIndex}: handling device`, device)
            this.updateDevice({ id: device.id, name: device.name, status: Status.LoggingIn })

            let hostname = ''
            let qrcode = ''

            // 如果当前设备有历史登录记录，用上次的 hostname 登录，这样可避免手机企微上出现“在新设备登录”的提示
            const prevLoginHostname = this.storage.get(device.id)
            if (truthy(prevLoginHostname)) {
                const res = await this.qywxs.containerLogin(prevLoginHostname, false)
                if (res.success) {
                    if (res.data.Status === 0) {
                        logger.debug(`task #${taskId} loop #${loopIndex}: 用历史 hostname 登录成功`, device)
                        this.loggedIn(device, res.data.HostName)
                        await sleep(DELAY_SECONDS * 1000)
                        continue
                    } else if (res.data.Status === 4) {
                        hostname = res.data.HostName
                        qrcode = res.data.QrCode
                        logger.debug(
                            `task #${taskId} loop #${loopIndex}: 用历史 hostname 生成了新的 hostname 和 qrcode`,
                            device,
                        )
                    } else {
                        logger.debug(`task #${taskId} loop #${loopIndex}: 尝试用历史 hostname 登录失败`, device)
                    }
                } else {
                    logger.debug(
                        `task #${taskId} loop #${loopIndex}: 尝试用历史 hostname 登录失败，${res.error}`,
                        device,
                    )
                }
            }

            if (!hostname || !qrcode) {
                // 常规方式取得容器二维码
                const containerRes = await this.container.request(`${taskId}-${loopIndex}`) // 每个 loop 获取容器时使用一个自己独有的 ownerId，以防冲突
                if (!containerRes.success) {
                    this.updateDevice({
                        id: device.id,
                        name: device.name,
                        status: Status.Failed,
                        error: containerRes.error,
                    })
                    continue
                }
                hostname = containerRes.data.hostname
                qrcode = containerRes.data.qrcode
            }

            // 执行实际的登录行为
            const loginSuccess = await this.login(device, hostname, qrcode)
            logger.debug(
                `task #${taskId} loop #${loopIndex}: finish handling device`,
                device,
                loginSuccess,
            )
            if (loginSuccess) await sleep(DELAY_SECONDS * 1000)
        }
        /* eslint-enable no-await-in-loop */
    }

    protected updateDevice(device: Device) {
        const devices = this.devices.value
        if (!devices.has(device.id)) return // 若设备已不在设备列表里，不执行更新（不然会导致它又被添加回来）

        this._devices.next(this.devices.value.set(device.id, device))
    }

    protected async login(device: Device, hostname: string, qrcode: string): Promise<boolean> {
        // 传给后端的 qrcode 必须保证是完整 url
        if (!qrcode.startsWith('http')) qrcode = `${location.protocol}//${location.host}${qrcode}`

        const res = await API<{ deviceId: string; errorMsg: string }>('/Device/LoginDevice', {
            data: {
                deviceId: device.id,
                qrCode: qrcode,
                hostname,
            },
        })

        const error = res.success ? res.data.errorMsg : res.error

        if (!error) {
            this.loggedIn(device, hostname)
            return true
        } else {
            this.updateDevice({ id: device.id, name: device.name, status: Status.Failed, error })
            return false
        }
    }
    protected loggedIn(device: Device, hostname: string) {
        this.storage.set(device.id, hostname)
        this.updateDevice({ id: device.id, name: device.name, status: Status.LoggedIn, hostname })
    }

    /**
     * 监听企业微信事件，同步更新设备状态
     */
    initListenQywxStatus() {
        this.qywxs.events.subscribe(event => {
            // 发现记录为“已登录”的企业微信掉线时，移除”已登录“状态
            // 这里不直接监听 removed，是因为如果批量登录时是把已经登录的企业微信顶掉了登录成功的，会触发一次 remove 事件，而导致批量登录记录的登录信息被清除
            const removeEvents = ['shutdown', 'destroying', 'loggingOut']
            if (removeEvents.includes(event.type)) {
                const { hostname } = event.info
                const device = this.devices.value.find(
                    item => item.status === Status.LoggedIn && item.hostname === hostname,
                )
                if (device) this.updateDevice({ id: device.id, name: device.name, status: Status.Idle })
            }
        })
    }
}
